<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebGL Fingerprinting - Camoufox</title>
    <style>
      html,
      body {
        height: 100%;
        margin: 0;
        padding: 0;
        overflow: hidden;
      }
      body {
        font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
        background-color: #1e1e1e;
        color: #d4d4d4;
        display: flex;
        flex-direction: column;
      }
      .container {
        flex: 1;
        display: flex;
        flex-direction: column;
        padding: 20px;
        overflow: hidden;
      }
      h1 {
        color: #569cd6;
        margin-top: 0;
      }
      #output {
        flex: 1;
        background-color: #252526;
        border: 1px solid #3e3e42;
        border-radius: 4px;
        padding: 15px;
        white-space: pre-wrap;
        word-wrap: break-word;
        font-family: "Consolas", "Courier New", monospace;
        font-size: 14px;
        overflow-y: auto;
      }
      .button-container {
        display: flex;
        gap: 10px;
        margin-top: 5px;
      }
      .btn {
        background-color: #0e639c;
        color: white;
        border: none;
        padding: 3px 8px;
        cursor: pointer;
        border-radius: 4px;
        font-size: 16px;
      }
      .btn:hover {
        background-color: #1177bb;
      }
      .string {
        color: #ce9178;
      }
      .number {
        color: #b5cea8;
      }
      .boolean {
        color: #569cd6;
      }
      .null {
        color: #569cd6;
      }
      .key {
        color: #9cdcfe;
      }
      #hash {
        margin-bottom: 5px;
        font-family: "Consolas", "Courier New", monospace;
        font-size: 14px;
        color: #569cd6;
      }
      #warning {
        background-color: #ff9800;
        color: #000;
        margin-bottom: 10px;
        border-radius: 4px;
        display: none;
        position: relative;
        transition: opacity 0.3s ease-out, max-height 0.3s ease-out,
          padding 0.3s ease-out;
        opacity: 1;
        max-height: 100px;
        overflow: hidden;
        padding: 10px;
      }
      #warning.hide {
        opacity: 0;
        max-height: 0;
        padding: 0;
      }
      #close-warning {
        position: absolute;
        top: 50%;
        right: 5px;
        transform: translateY(-50%);
        background: none;
        border: none;
        color: #000;
        font-size: 16px;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Your WebGL Information (in Camoufox format)</h1>
      <div id="warning">
        Warning: This browser is not Firefox. The fingerprint below will not run
        properly on Camoufox.<button id="close-warning">&times;</button>
      </div>
      <div id="hash"></div>
      <pre id="output"></pre>
      <div class="button-container">
        <button id="copy-btn" class="btn">Copy data</button>
        <button id="copy-minified-btn" class="btn">Copy data (minified)</button>
      </div>
    </div>

    <script>
      const dumpWebGLCore = async (
        webglContextId,
        experimentalWebglContextId
      ) => {
        function getWebGLContext() {
          const canvas = new OffscreenCanvas(256, 256);
          let result = null;

          try {
            result =
              canvas.getContext(webglContextId, {
                preserveDrawingBuffer: false,
              }) ||
              canvas.getContext(experimentalWebglContextId, {
                preserveDrawingBuffer: false,
              });
          } catch (ex) {}

          result || (result = null);
          return result;
        }

        const webglContext = getWebGLContext();

        if (!webglContext) {
          return {};
        }

        const glEnums = [
          2849, 2884, 2885, 2886, 2928, 2929, 2930, 2931, 2932, 2960, 2961,
          2962, 2963, 2964, 2965, 2966, 2967, 2968, 2978, 3024, 3042, 3074,
          3088, 3089, 3106, 3107, 3314, 3315, 3316, 3317, 3330, 3331, 3332,
          3333, 3379, 3386, 3408, 3410, 3411, 3412, 3413, 3414, 3415, 7936,
          7937, 7938, 10752, 32773, 32777, 32823, 32824, 32873, 32877, 32878,
          32883, 32926, 32928, 32936, 32937, 32938, 32939, 32968, 32969, 32970,
          32971, 33000, 33001, 33170, 33901, 33902, 34016, 34024, 34045, 34047,
          34068, 34076, 34467, 34816, 34817, 34818, 34819, 34852, 34853, 34854,
          34855, 34856, 34857, 34858, 34859, 34860, 34877, 34921, 34930, 34964,
          34965, 35071, 35076, 35077, 35371, 35373, 35374, 35375, 35376, 35377,
          35379, 35380, 35657, 35658, 35659, 35660, 35661, 35723, 35724, 35725,
          35738, 35739, 35968, 35977, 35978, 35979, 36003, 36004, 36005, 36006,
          36007, 36063, 36183, 36203, 36345, 36347, 36348, 36349, 36387, 36388,
          36392, 36795, 37137, 37154, 37157, 37440, 37441, 37443, 37444, 37445,
          37446, 37447, 38449,
        ];

        let rendererInfo = null;
        const debug_ext = webglContext.getExtension(
          "WEBGL_debug_renderer_info"
        );
        if (debug_ext) {
          rendererInfo = {
            webglVendor: webglContext.getParameter(
              debug_ext.UNMASKED_VENDOR_WEBGL
            ),
            webglRenderer: webglContext.getParameter(
              debug_ext.UNMASKED_RENDERER_WEBGL
            ),
          };
        }

        const result = {
          [`${webglContextId}:renderer`]: rendererInfo
            ? rendererInfo.webglRenderer
            : "Not available",
          [`${webglContextId}:vendor`]: rendererInfo
            ? rendererInfo.webglVendor
            : "Not available",
          [`${webglContextId}:contextAttributes`]:
            webglContext.getContextAttributes(),
          [`${webglContextId}:supportedExtensions`]:
            webglContext.getSupportedExtensions() || [],
          [`${webglContextId}:parameters`]: {},
          [`${webglContextId}:shaderPrecisionFormats`]: {},
        };
        for (const glEnum of glEnums) {
          try {
            const parmValue = webglContext.getParameter(glEnum);
            if (
              Array.isArray(parmValue) ||
              [Float32Array, Int32Array, Uint32Array].some(
                (type) => parmValue instanceof type
              )
            ) {
              const arrayValue = Array.from(parmValue);
              result[`${webglContextId}:parameters`][glEnum] =
                arrayValue.length > 0 ? arrayValue : null;
            } else {
              result[`${webglContextId}:parameters`][glEnum] = parmValue;
            }
          } catch (err) {
            console.log(err);
          }
        }

        const shaderTypes = [
          webglContext.VERTEX_SHADER,
          webglContext.FRAGMENT_SHADER,
        ];
        const precisionTypes = [
          webglContext.LOW_FLOAT,
          webglContext.MEDIUM_FLOAT,
          webglContext.HIGH_FLOAT,
          webglContext.LOW_INT,
          webglContext.MEDIUM_INT,
          webglContext.HIGH_INT,
        ];

        for (let shaderType of shaderTypes) {
          for (let precisionType of precisionTypes) {
            const r = webglContext.getShaderPrecisionFormat(
              shaderType,
              precisionType
            );
            const key = `${shaderType},${precisionType}`;
            result[`${webglContextId}:shaderPrecisionFormats`][key] = {
              rangeMin: r.rangeMin,
              rangeMax: r.rangeMax,
              precision: r.precision,
            };
          }
        }

        return result;
      };

      function syntaxHighlight(json) {
        json = json
          .replace(/&/g, "&amp;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;");
        return json.replace(
          /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
          function (match) {
            var cls = "number";
            if (/^"/.test(match)) {
              if (/:$/.test(match)) {
                cls = "key";
              } else {
                cls = "string";
              }
            } else if (/true|false/.test(match)) {
              cls = "boolean";
            } else if (/null/.test(match)) {
              cls = "null";
            }
            return '<span class="' + cls + '">' + match + "</span>";
          }
        );
      }

      async function sha256(message) {
        const msgBuffer = new TextEncoder().encode(message);
        const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
        const hashArray = Array.from(new Uint8Array(hashBuffer));
        const hashHex = hashArray
          .map((b) => b.toString(16).padStart(2, "0"))
          .join("");
        return hashHex;
      }

      document.addEventListener(
        "DOMContentLoaded",
        async () => {
          // Check if the user agent is Firefox
          const isFirefox =
            navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
          if (!isFirefox) {
            document.getElementById("warning").style.display = "block";
          }

          // Add event listener for close button
          document
            .getElementById("close-warning")
            .addEventListener("click", function () {
              document.getElementById("warning").classList.add("hide");
            });

          const [webglDetail, webgl2Detail] = await Promise.all([
            dumpWebGLCore("webgl", "experimental-webgl"),
            dumpWebGLCore("webgl2", "experimental-webgl2"),
          ]);

          const combinedInfo = { ...webglDetail, ...webgl2Detail };
          const jsonString = JSON.stringify(combinedInfo, null, 2);
          const minifiedJson = JSON.stringify(combinedInfo);
          const hashValue = await sha256(minifiedJson);

          document.getElementById("hash").textContent = `SHA256: ${hashValue}`;
          document.getElementById("output").innerHTML =
            syntaxHighlight(jsonString);

          document.getElementById("copy-btn").addEventListener("click", () => {
            navigator.clipboard.writeText(jsonString).then(() => {
              alert("Copied formatted JSON to clipboard!");
            });
          });

          document
            .getElementById("copy-minified-btn")
            .addEventListener("click", () => {
              navigator.clipboard.writeText(minifiedJson).then(() => {
                alert("Copied minified JSON to clipboard!");
              });
            });
        },
        false
      );
    </script>
  </body>
</html>
